// CR Log 1055 Method getHost() is added by MLin on June 2003
// Copyright (c) 2002 HTHK

package com.ffpc.sql;

import java.io.PrintWriter;
import java.net.URL;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.log4j.Logger;

/**
 * <p>Generic database connection factory for Java applications. 
 * Supports external configuration through Jakarta Commons Configuration API.</p>
 * 
 * <p>One or more connection providers can be configured to provide connections. 
 * Typically, each provider maps to different data sources, though multiple
 * providers can be mapped to the same data source with different configuration
 * settings (if the type of provider supports it).</p>
 * 
 * <p>To initialize the factory, call one of the following methods:</p>
 * <ol>
 * <li>{@link #initialize(Configuration)}</li>
 * <li>{@link #initialize(String)}</li>
 * </ol>
 * 
 * <p>During class loading, the factory will attempt to initialize itself by looking
 * for the following files (in given order) in the class path:</p>
 * <ul>
 * <li>com/hthk/ffpc/sql/ConnectionFactory.properties</li>
 * <li>ConnectionFactory.properties</li>
 * </ul>
 * 
 * <p>If auto-initialization succeeded, a JVM shutdown hook will be registered to
 * call {@link #shutdown()} when the JVM terminates. This is probably not desirable
 * in environments such as servlet containers, and therefore should be avoided.
 * The feature was implemented for legacy Java applications that do not explicitly
 * initialize and shutdown the factory.</p>
 * 
 * <p>See {@link #initialize(Configuration)} for general configuration guide.</p>
 * 
 * <p>General usage:</p>
 * <pre>
 *     ConnectionFactory cf = new ConnectionFactory("provider");
 *     Connection conn = cf.getConnection();
 *     ...
 *     
 *     try {
 *         ...
 *     } finally {
 *         ...
 *         cf.close();  // or conn.close()
 *     }
 * </pre>
 *
 * <p>It is particularly important to close the connection after using it.</p>
 *
 * <p>Note that contrary to what you might think about how a "factory" should behave, 
 * a ConnectionFactory instance produce one and only one Connection, in other words,
 * getConnection() always returns the same instance. Such behavior is deliberately
 * maintained for backward-compatibility.</p>
 * 
 * <p>This class is not thread-safe. Avoid using the same instance in multiple threads.</p>
 * 
 * @author HTHK
 * @version $Id: ConnectionFactory.java,v 1.1.1.1 2013/08/16 05:35:48 tengyang Exp $
 */
public class ConnectionFactory
{
    private static final Logger logger = Logger.getLogger(ConnectionFactory.class);

    private static HashMap aliases = new HashMap();
    private static HashMap connectionProviders = new HashMap();

    private static Configuration factoryConfig;
    private static String defaultProvider;
    
    private ConnectionProvider connectionProvider = null;
    private String providerName;
    private Connection con = null;

    static {
        initialize();
    }
    
    /**
     * <p>Initializes the factory with the given configuration.<p>
     * 
     * <p>One or more providers can be defined by the key <em>db.provider.NAME.className</em>, 
     * where NAME is a unique identifier of the provider, with the provider class name as the
     * value. A provider class is any class that implements the ConnectionProvider interface.</p>
     * 
     * <p>Each providers are configured by keys with the prefix <em>db.provider.NAME</em>. See
     * documentation of respective providers for supported configuration keys.</p>
     * 
     * <p>A default provider must be specified with the key <em>db.provider.default</em> to 
     * support the legacy no-arg constructor. It can be set to an alias (see below).</p>
     * 
     * <p>Virtual provider names (aliases) can be defined to provide a layer of indirection and makes
     * the configuration more flexible. They are defined by the key <em>db.provider.ALIAS.alias</em>,
     * where ALIAS is the virtual name, with the actual provider name as the value:</p>
     * 
     * <pre>
     * db.provider.DEV.className=...
     * db.provider.UAT.className=...
     * db.provider.APP.alias=DEV
     * </pre>
     * 
     * <p>With this feature the active provider can be switched by simply changing the alias key,
     * without modifying the source (provided that the alias is used in the source).</p>
     * 
     * @param config Configuration key-value pairs. There are multiple ways to create Configuration objects. 
     *          See Jakarta Commons Configuration documentation for detail.
     */
    public static void initialize(Configuration config) {
        factoryConfig = config.subset("db.provider");
        defaultProvider = factoryConfig.getString("default");
        
        if (defaultProvider == null)
            logger.warn("Default connection provider is not specified");
        else
            logger.info("Default connection provider: " + defaultProvider);

        logger.info("Initializing connection providers...");
        
        for (Iterator keys = factoryConfig.getKeys(); keys.hasNext(); ) {
            String key = (String) keys.next();
            
            if (key.indexOf('.') == -1)
                continue;
            
            int j = key.lastIndexOf('.');       //if key = x.y.z
            String name = key.substring(0, j);  //then name = x.y

            if (key.endsWith(".className")) {
                logger.info("Initializing provider " + name);
                factoryConfig.setProperty(name + ".name", name);
                
                try {
                    ConnectionProvider cp = getConnectionProvider(name);
                    logger.debug("Created provider " + cp);
                } catch (DataException e) {
                    logger.error("Connection provider " + name + " cannot be initialized", e);
                }
            }

            if (key.endsWith(".alias")) {
                String value = factoryConfig.getString(key);
                if (value != null) {
                    if (name.equals(value))
                        logger.warn(name + " aliases itself.");
                    else
                        aliases.put(name, value);
                }
            }
        }

        logger.info("Connection providers are ready.");
    }
    
    /**
     * Initializes the factory with the given configuration file. This is equivalent to
     * initialize(new PropertiesConfiguration(file)).
     * 
     * @param file Absolute path to the configuration file.
     * @throws ConfigurationException 
     */
    public static void initialize(String file) throws ConfigurationException {
        logger.info("Initializing with configuration file " + file);
        initialize(new PropertiesConfiguration(file));
    }

    /**
     * Automatic initialization by looking for ConnectoryFactory.property in the class path. 
     * This is called at class loading and thus need not be called explicitly. The factory
     * will shutdown when the JVM terminates.
     */
    static void initialize() {
        try {
            Class<ConnectionFactory> clazz = ConnectionFactory.class;
            
            // com.hthk.ffpc.sql.ConnectionFactory.properties
            URL url = clazz.getResource("ConnectionFactory.properties");
            
            // ConnectionFactory.properties
            if (url == null) {
                url = clazz.getResource("/ConnectionFactory.properties");
            }
            
            if (url != null) {
                logger.info("Initializing with " + url);
                initialize(new PropertiesConfiguration(url));
                
                Runtime.getRuntime().addShutdownHook(new Thread() {
                    @Override
					public void run() {
                        ConnectionFactory.shutdown();
                    }
                });
            }
        } catch (ConfigurationException e) {
            logger.error("", e);
        }
    }
    
    /**
     * Shuts down all connection providers.
     */
    public static void shutdown() {
        logger.info("Shutting down connection providers...");
        
        for (Iterator i = connectionProviders.values().iterator(); i.hasNext(); ) {
            ConnectionProvider cp = (ConnectionProvider) i.next();
            logger.debug("Shutting down provider " + cp);
            logger.info("Shutting down provider " + cp.getName());
            cp.destroy();
        }

        logger.info("Connection providers shutdown complete.");
    }

    /**
     * Writes the status of the factory and all the providers to the given stream.
     * 
     * @param out A character stream.
     */
    public static void diagnose(PrintWriter out) {
        out.println(new Date());
        out.println();
        out.println("Default connection provider: " + defaultProvider);
        out.println();
        
        for (Iterator i = aliases.keySet().iterator(); i.hasNext(); ) {
            String key = (String) i.next();
            out.println(key + " is alias for " + aliases.get(key));
        }
        
        out.println();
        
        for (Iterator i = connectionProviders.values().iterator(); i.hasNext(); ) {
            ConnectionProvider cp = (ConnectionProvider) i.next();
            cp.diagnose(out);
        }

        out.flush();
    }
    
    /**
     * Returns the actual provider name of the given alias. If the given value is not an alias,
     * it will be returned as-is.
     * 
     * @param alias
     */
    private static String resolveAlias(String alias) {
        String name = (String) aliases.get(alias);
        return name != null ? name : alias;
    }

    /**
     * Return the named connection provider. If the provider is not yet created, it will be created
     * with the information given in factoryConfig.
     * 
     * @param name Name of the provider.
     * @return A connection provider.
     * @throws DataException if any required information is not given, or there is exception
     *                       while connecting to the database.
     */
    private synchronized static ConnectionProvider getConnectionProvider(String name) throws DataException {
        ConnectionProvider cp = (ConnectionProvider) connectionProviders.get(name);
        if (cp != null)
            return cp;
        
        Configuration cpcfg = factoryConfig.subset(name);
        String className = cpcfg.getString("className");
        
        try {
            Class<?> clazz = Class.forName(className);
            cp = (ConnectionProvider) clazz.newInstance();
        } catch (Exception e) {
            logger.error("Error instantiating connection provider", e);
            throw new DataException(e);
        }

        try {
            cp.init(cpcfg);
        } catch (SQLException e) {
            logger.error("Error initializing connection provider", e);
            throw new DataException(e);
        }
        
        connectionProviders.put(name, cp);
        return cp;
    }
    
    /**
     * Creates a factory using the default connection provider. This is for legacy apps only.
     * Use {@link #ConnectionFactory(String)} instead.
     * 
     * @throws DataException
     */
    public ConnectionFactory() throws DataException {
        this(defaultProvider);
    }

    /**
     * Creates a factory using the named connection provider.
     * 
     * @param name Name of the connection provider.
     * @throws DataException
     */
    public ConnectionFactory(String name)
      throws DataException
    {
        try
        {
            providerName = resolveAlias(name);

            if (!connectionProviders.containsKey(providerName)) 
                throw new DataException("Unknown connection provider " + providerName);
            
            connectionProvider = getConnectionProvider(providerName);
            
            setConnection(connectionProvider.getConnection(this));
            setAutoCommit(false);
        }
        catch (SQLException e)
        {
            logger.error(e);
            throw new DataException(e);
        }
        catch (RuntimeException e) {
            logger.error(e);
            throw e;
        }
    }

    // for internal use only
    void setConnection(Connection con)
    {
        this.con = con;
    }

    /**
     * <p>Returns the Connection object associated with this instance. The Connection object returned 
     * must ultimately be closed either by calling {@link #close()} or {@link Connection#close()} to 
     * release underlying system resources. Once the connection is closed, this method returns
     * null and a new instance is required to obtain another connection.</p>
     * 
     * @return the Connection object associated with this instance, or null if the connection is closed.
     */
    public final Connection getConnection()
    {
        return this.con;
    }

    /**
     * Rolls back the underlying connection.
     * 
     * @throws DataException
     */
    public void rollback()
      throws DataException
    {
        try
        {
            if (getConnection()!=null)
                getConnection().rollback();
        }
        catch (SQLException e)
        {
            throw new DataException(e);
        }
    }

    /**
     * Commits the underlying connection.
     *  
     * @throws DataException
     */
    public void commit()
      throws DataException
    {
        try
        {
            if (getConnection()!=null)
                getConnection().commit();
        }
        catch (SQLException e)
        {
            throw new DataException(e);
        }
    }

    private void setAutoCommit(boolean isAutoCommit)
      throws DataException
    {
        try
        {
          if (getConnection()!=null)
              getConnection().setAutoCommit(isAutoCommit);
        }
        catch (SQLException e)
        {
            throw new DataException(e);
        }
    }

    @Override
	public String toString()
    {
       if (connectionProvider!=null)
       {
          return connectionProvider.toString();
       }
       else
          return new String("Connection provider is not available");
    }
    
    /**
     * Convenience method for closing a result set. It will emit a warning instead of 
     * throwing an exception if it fails to close the result set.
     * 
     * @param rs
     */
    public void close(ResultSet rs) {
        try {
            if (rs != null)
                rs.close();
        } catch (SQLException e) {
            logger.warn(e);
        }
    }
    
    /**
     * Convenience method for closing a statement. It will emit a warning instead of 
     * throwing an exception if it fails to close the statement.
     * 
     * @param rs
     */
    public void close(Statement s) {
        try {
            if (s != null)
                s.close();
        } catch (SQLException e) {
            logger.warn(e);
        }
    }
    
    /**
     * Closes the underlying connection. Whether the connection is physically closed 
     * is provider-specific. Basically the same as getConnection().close() except that 
     * any exceptions thrown is suppressed.
     */
    public void close() {
        if (getConnection()!=null)
            try {
                getConnection().close();
            } catch (SQLException e) {
                // No exception would be thrown
            }
        setConnection(null);
    }

    /**
     * <p><em>Commits</em> and closes the underlying connection.</p>
     * 
     * <p>The implicit commit is retained as large portion of the existing code base depends on it.
     * For new applications it is recommended to use {@link #close()} instead.</p>
     * 
     * <p>Generally application code should not explicitly call Object.finalize(). It is reserved
     * for the JVM only. The method should not be defined with public visibility as well. This
     * method is retained for existing code base only.</p>
     */
    @Override
	public void finalize()
      throws DataException
    {
        commit();
        setAutoCommit(true);
        close();
    }
    public String getUsername() 
    {
        Connection con = getConnection();
        
        if (con != null) {
            try {
                if (!con.isClosed())
                    return getConnection().getMetaData().getUserName();
            } catch (SQLException e) {
                logger.error(e);
            }
        }
        
        return "-";
    }

    /**
     * Deprecated. Always return a lone dash.
     * 
     * @return a lone dash.
     * @deprecated
     */
    @Deprecated
	public String getPassword() 
    {
      return "-";
    }

    /**
     * Deprecated. Always return the provider name.
     * 
     * @return the provider name.
     * @deprecated
     */
    @Deprecated
	public String getSID()
    {
      return defaultProvider;
    }

    public String getHost()
    {
        Connection con = getConnection();
        
        if (con != null) {
            try {
                if (!con.isClosed())
                    return getConnection().getMetaData().getURL();
            } catch (SQLException e) {
                logger.error(e);
            }
        }
        
        return "-";
    }

}

